package com.thunder.ktv.scoreengine2test.ui;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;

import androidx.annotation.Nullable;

import com.thunder.ktv.scoreengine2sdk.ScoreEngine2;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 * 自定义卡拉OK打分条视图。
 * 功能包括：
 * 1. 显示滚动的音调条和歌词。
 * 2. 实时显示用户演唱的音高。
 * 3. 根据演唱情况对音调条进行实时和最终的染色。
 * 4. 包含多种动画效果，如击中光圈、得分闪烁和连击计数。
 */
public class ScoreBarView extends View {

    private static final String TAG = "ScoreBarView";

    // --- 核心参数，用于时间与帧的转换 ---
    /** PCM音频采样率，用于计算时间 */
    private static final float PCM_SAMPLE_RATE = 48000f;
    /** PCM音频每帧的采样点数 */
    private static final int PCM_FRAME_SIZE = 1024;
    /** 每秒对应的帧数 */
    private static final float FRAMES_PER_SECOND = PCM_SAMPLE_RATE / PCM_FRAME_SIZE;

    // --- UI与行为配置 ---
    /** 视图滚动的速度，单位：像素/秒 */
    private float mPixelsPerSecond = 150f;
    /** 时间指针在视图宽度上的位置比例 (例如 0.4f 表示在从左到右40%的位置) */
    private final float mCursorPositionRatio = 0.4f;
    /** 音调条的高度 (dp) */
    private final float mPitchBarHeightDp = 6f;
    /** 用户音高指示球的半径 (dp) */
    private final float mUserPitchBallRadiusDp = 6f;
    /** 歌词文字大小 (sp) */
    private final float mLyricTextSizeSp = 14f;
    /** 歌词距离音调条顶部的间距 (dp) */
    private final float mLyricTextMarginTopDp = 4f;

    // --- 颜色配置 ---
    private final int mColorPitchBarDefault = Color.WHITE; // 默认音调条颜色
    private final int mColorPitchBarHit = Color.GREEN; // 击中时的颜色
    private final int mColorPitchBarMiss = Color.GRAY; // 未击中时的颜色
    private final int mColorCursor = Color.parseColor("#FFD700"); // 时间指针线和默认小球颜色
    private final int mColorLyric = Color.WHITE; // 歌词颜色
    private final int mColorBackground = Color.parseColor("#80000000"); // 背景色（半透明黑）
    private final int mColorGuideLine = Color.parseColor("#4DFFFFFF"); // 音高参考线颜色

    // --- 逻辑配置 ---
    /** 音高匹配的容差范围 (±2.0) */
    private final float mPitchHitTolerance = 2.0f;
    /** 底部时间轴的刻度间隔 (秒) */
    private int mTimelineTickSeconds = 5;
    /** 是否启用最终整体染色行为的开关 */
    private boolean mEnableFinalizeColor = false;

    // --- 数据与状态变量 ---
    /** 经过预处理后，按句子组织的歌词数据列表 */
    private List<Sentence> mSentences = new ArrayList<>();
    /** 整首歌的最低和最高音高，用于确定Y轴范围 */
    private float mMinPitch = Float.MAX_VALUE, mMaxPitch = Float.MIN_VALUE;
    /** Y轴实际显示的最低和最高音高（经过规整） */
    private float mDisplayMinPitch = 40f, mDisplayMaxPitch = 80f;
    /** 当前播放的帧索引，由外部传入，驱动整个视图滚动 */
    private volatile long mCurrentFrameIndex = 0;
    /** 用户当前的实时音高，由外部传入 */
    private volatile float mCurrentUserPitch = 0f;

    // --- 绘制工具 ---
    /** 各种画笔，每种颜色或样式对应一个，以提高效率 */
    private Paint mPaintPitchBarDefault, mPaintPitchBarHit, mPaintPitchBarMiss;
    private Paint mPaintCursor, mPaintUserPitchBall, mPaintLyric, mPaintGuideLine;
    private Paint mPaintTimeline, mBitmapPaint;
    /** 可复用的对象，避免在 onDraw 中创建新对象，减少GC压力 */
    private Rect mTextBounds = new Rect();
    private Rect mSrcRect = new Rect();
    private RectF mDestRectF = new RectF();
    private Path mRoundRectPath = new Path();
    private RectF mClipRectF = new RectF();

    // --- 动画相关变量 ---
    private Paint mPaintHitGlow; // 击中光圈的画笔
    private long mLastHitTime = 0; // 上次音高匹配的时间戳
    private final long HIT_GLOW_DURATION = 300; // 光圈动画持续时间
    private int mComboCount = 0; // 连击计数
    private Paint mPaintComboText;
    private float mComboTextSize = 0; // 用于实现跳动动画的文字大小
    private long mLastComboTime = 0; // 上次增加连击的时间戳
    private final long COMBO_ANIMATION_DURATION = 200; // Combo跳动动画持续时间
    private final long SCORE_FLASH_DURATION = 400; // 单字得分闪烁动画持续时间

    // --- 视图尺寸变量 ---
    private int mViewWidth, mViewHeight;
    private float mCursorPositionX, mPitchBarHeightPx, mUserPitchBallRadiusPx, mLyricTextMarginTopPx;

    public ScoreBarView(Context context) {
        super(context);
        init(context);
    }

    public ScoreBarView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ScoreBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    /**
     * 初始化画笔、尺寸等资源。
     */
    private void init(Context context) {
        // 初始化各种画笔
        mPaintPitchBarDefault = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintPitchBarDefault.setColor(mColorPitchBarDefault);

        mPaintPitchBarHit = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintPitchBarHit.setColor(mColorPitchBarHit);
        mPaintPitchBarHit.setStrokeWidth(2f); // 设置线宽，用于在离屏Bitmap上画出实心的线

        mPaintPitchBarMiss = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintPitchBarMiss.setColor(mColorPitchBarMiss);
        mPaintPitchBarMiss.setStrokeWidth(2f);

        mPaintCursor = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintCursor.setColor(mColorCursor);
        mPaintCursor.setStrokeWidth(dpToPx(2));

        mPaintUserPitchBall = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintUserPitchBall.setColor(mColorCursor);

        mPaintLyric = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintLyric.setColor(mColorLyric);
        mPaintLyric.setTextSize(spToPx(mLyricTextSizeSp));
        mPaintLyric.setTextAlign(Paint.Align.CENTER);

        mPaintGuideLine = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintGuideLine.setColor(mColorGuideLine);
        mPaintGuideLine.setStrokeWidth(dpToPx(0.5f));

        mPaintTimeline = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintTimeline.setColor(Color.LTGRAY);
        mPaintTimeline.setTextSize(spToPx(10f));
        mPaintTimeline.setTextAlign(Paint.Align.CENTER);

        mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBitmapPaint.setFilterBitmap(true); // 开启双线性过滤，让拉伸后的Bitmap更平滑

        // 初始化动画画笔
        mPaintHitGlow = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintHitGlow.setStyle(Paint.Style.STROKE);
        mPaintHitGlow.setColor(mColorPitchBarHit);
        mPaintHitGlow.setStrokeWidth(dpToPx(2));

        mPaintComboText = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintComboText.setColor(Color.WHITE);
        mPaintComboText.setTextAlign(Paint.Align.RIGHT);
        mPaintComboText.setFakeBoldText(true);
        mPaintComboText.setShadowLayer(dpToPx(2), 1, 1, Color.BLACK); // 给文字加阴影，更具立体感

        // 将dp/sp单位转换为像素值
        mPitchBarHeightPx = dpToPx(mPitchBarHeightDp);
        mUserPitchBallRadiusPx = dpToPx(mUserPitchBallRadiusDp);
        mLyricTextMarginTopPx = dpToPx(mLyricTextMarginTopDp);
    }

    /**
     * 设置是否在单字演唱结束后，使用最终得分颜色统一覆盖整个音调条。
     *
     * @param enable true表示统一染色（默认），false表示保留演唱过程的细节颜色。
     */
    public void setEnableFinalizeColor(boolean enable) {
        this.mEnableFinalizeColor = enable;
    }

    /**
     * 获取当前连击数。
     */
    public int getComboCount() {
        return mComboCount;
    }

    /**
     * 外部接口：初始化歌词数据。
     *
     * @param charEntries 原始的、包含所有歌词信息的列表。
     */
    public void initCharEntries(List<ScoreEngine2.CharEntry> charEntries) {
        reset(); // 开始新歌曲前，先清空旧数据
        if (charEntries == null || charEntries.isEmpty()) {
            postInvalidate();
            return;
        }

        // 预处理：将原始列表按句子拆分，并计算全局音高范围
        List<DisplayCharEntry> currentSentenceChars = new ArrayList<>();
        for (ScoreEngine2.CharEntry entry : charEntries) {
            if (entry.startFrame == -1) { // 遇到分隔符，说明一句结束
                if (!currentSentenceChars.isEmpty()) {
                    mSentences.add(new Sentence(currentSentenceChars));
                    currentSentenceChars = new ArrayList<>();
                }
            } else {
                if (entry.pitch > 0) { // 收集有效音高
                    mMinPitch = Math.min(mMinPitch, entry.pitch);
                    mMaxPitch = Math.max(mMaxPitch, entry.pitch);
                }
                currentSentenceChars.add(new DisplayCharEntry(entry));
            }
        }
        if (!currentSentenceChars.isEmpty()) {
            mSentences.add(new Sentence(currentSentenceChars));
        } // 添加最后一句话

        calculatePitchDisplayRange(); // 根据min/max pitch计算一个合适的Y轴显示范围
        Log.d(TAG, "Initialized " + mSentences.size() + " sentences.");
        postInvalidate(); // 请求重绘
    }

    public List<Sentence> getSentences() {
        return mSentences;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        reset(); // View被销毁时，确保回收所有资源，特别是Bitmap
    }

    /**
     * 重置所有状态和数据，用于开始新歌曲或退出界面。
     */
    public void reset() {
        for (Sentence sentence : mSentences) {
            for (DisplayCharEntry charEntry : sentence.chars) {
                charEntry.recycle();
            }
        }
        mSentences.clear();
        mMinPitch = Float.MAX_VALUE;
        mMaxPitch = Float.MIN_VALUE;
        mDisplayMinPitch = 40f;
        mDisplayMaxPitch = 80f;
        mCurrentFrameIndex = 0;
        mCurrentUserPitch = 0;
        // 重置动画状态
        mComboCount = 0;
        mLastHitTime = 0;
        mLastComboTime = 0;
        postInvalidate();
    }

    /**
     * 外部接口：实时更新播放进度和用户音高。这是驱动视图更新的核心。
     *
     * @param frameIndex 当前播放帧。
     * @param pitch      用户当前音高。
     */
    public void setVoicePitch(int frameIndex, float pitch) {
        this.mCurrentFrameIndex = frameIndex;
        this.mCurrentUserPitch = pitch;
        updateScoring(frameIndex, pitch); // 更新打分逻辑和动画状态
        postInvalidate(); // 请求重绘
    }

    /**
     * 更新打分状态的核心逻辑，通常在非UI线程被频繁调用。
     */
    private void updateScoring(long frameIndex, float pitch) {
        boolean isCurrentFrameHitting = false; // 标记当前帧是否击中，用于触发光圈动画

        // 遍历所有句子和字，更新状态
        for (Sentence sentence : mSentences) {
            if (frameIndex < sentence.startFrame - 200)
                break; // 性能优化：如果还未到这句，后面的也不用看了
            if (frameIndex > sentence.endFrame + 200)
                continue; // 性能优化：如果已远超这句，跳过
            for (DisplayCharEntry charEntry : sentence.chars) {
                if (charEntry.isScored())
                    continue; // 如果已最终计分，跳过

                long startFrame = charEntry.originalEntry.startFrame;
                long endFrame = charEntry.originalEntry.endFrame;

                if (frameIndex >= startFrame && frameIndex < endFrame) { // 正在演唱这个字
                    if (pitch > 0) {
                        charEntry.userPitches.add(pitch); // 收集音高，为最终结算做准备
                        boolean isHit = Math.abs(pitch - charEntry.originalEntry.pitch) <= mPitchHitTolerance;
                        charEntry.drawScoreSegment(frameIndex, isHit ? mPaintPitchBarHit : mPaintPitchBarMiss); // 在离屏Bitmap上染色
                        if (isHit)
                            isCurrentFrameHitting = true; // 标记击中
                    }
                } else if (frameIndex >= endFrame && charEntry.scoreState == ScoreState.SCORING) { // 刚刚唱完这个字
                    boolean finalHit;
                    if (charEntry.userPitches.isEmpty()) {
                        finalHit = false;
                    } else {
                        float medianPitch = calculateMedian(charEntry.userPitches);
                        finalHit = Math.abs(medianPitch - charEntry.originalEntry.pitch) <= mPitchHitTolerance;
                    }

                    // 触发Combo和得分动画
                    if (finalHit) {
                        mComboCount++;
                        mLastComboTime = System.currentTimeMillis();
                    } else {
                        mComboCount = 0;
                    }
                    charEntry.scoreFinalizedTime = System.currentTimeMillis(); // 记录得分时间点

                    // 根据开关决定是否执行最终染色
                    if (mEnableFinalizeColor) {
                        charEntry.finalizeScore(finalHit ? mPaintPitchBarHit : mPaintPitchBarMiss);
                    } else {
                        charEntry.setFinalStateOnly(finalHit);
                    }
                }
            }
        }

        // 如果当前帧是击中状态，更新光圈动画的起始时间
        if (isCurrentFrameHitting) {
            if (System.currentTimeMillis() - mLastHitTime > HIT_GLOW_DURATION) {
                mLastHitTime = System.currentTimeMillis();
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(mColorBackground); // 绘制背景
        if (mSentences.isEmpty())
            return;

        // 依次绘制各个图层
        drawGuideLines(canvas);
        drawPitchBarsAndLyrics(canvas);
        drawTimeline(canvas);
        canvas.drawLine(mCursorPositionX, 0, mCursorPositionX, mViewHeight, mPaintCursor);

        float userPitchY = (mCurrentUserPitch > mDisplayMinPitch) ? pitchToY(mCurrentUserPitch)
                : mViewHeight - mUserPitchBallRadiusPx;
        updateUserPitchBallColor();

        // 绘制动画层
        drawHitGlowAnimation(canvas, userPitchY);
        canvas.drawCircle(mCursorPositionX, userPitchY, mUserPitchBallRadiusPx, mPaintUserPitchBall); // 小球在光圈上
        drawComboAnimation(canvas);
    }

    /**
     * 绘制音调条和歌词的主方法。
     */
    private void drawPitchBarsAndLyrics(Canvas canvas) {
        // 性能优化：计算可视区域对应的帧范围，只绘制屏幕内的元素
        long visibleStartFrame = xToFrame(0);
        long visibleEndFrame = xToFrame(mViewWidth);

        for (Sentence sentence : mSentences) {
            if (sentence.endFrame < visibleStartFrame || sentence.startFrame > visibleEndFrame)
                continue;
            for (DisplayCharEntry charEntry : sentence.chars) {
                if (charEntry.originalEntry.endFrame < visibleStartFrame
                        || charEntry.originalEntry.startFrame > visibleEndFrame)
                    continue;

                // 计算坐标
                final long startFrame = charEntry.originalEntry.startFrame;
                final long endFrame = charEntry.originalEntry.endFrame;
                final long duration = endFrame - startFrame;
                float startX = frameToX(startFrame);
                float endX = frameToX(endFrame);
                float pitchY = pitchToY(charEntry.originalEntry.pitch);

                // 处理得分闪烁动画，动态改变音调条高度
                float currentBarHeight = mPitchBarHeightPx;
                long timeSinceScored = System.currentTimeMillis() - charEntry.scoreFinalizedTime;
                boolean isFlashing = charEntry.scoreFinalizedTime > 0 && charEntry.scoreState == ScoreState.HIT
                        && timeSinceScored < SCORE_FLASH_DURATION;
                if (isFlashing) {
                    float progress = timeSinceScored / (float) SCORE_FLASH_DURATION;
                    float scale = 1.0f + 0.3f * (float) Math.sin(progress * Math.PI); // 使用正弦曲线实现放大再缩小的效果
                    currentBarHeight *= scale;
                }
                float topY = pitchY - currentBarHeight / 2;
                float bottomY = pitchY + currentBarHeight / 2;

                // 使用 clipPath 实现圆角效果
                canvas.save(); // 保存当前画布状态
                mRoundRectPath.reset();
                float cornerRadius = currentBarHeight / 2f; // 半径为高度一半，形成完美圆角
                mClipRectF.set(startX, topY, endX, bottomY);
                mRoundRectPath.addRoundRect(mClipRectF, cornerRadius, cornerRadius, Path.Direction.CW);
                canvas.clipPath(mRoundRectPath); // 应用裁剪，之后的所有绘制都会被限制在这个圆角矩形内

                // 执行无覆盖的绘制逻辑
                if (mCurrentFrameIndex < startFrame || duration <= 0 || charEntry.scoreBitmap == null) { // 情况1: 字还未开始
                    canvas.drawRect(startX, topY, endX, bottomY, mPaintPitchBarDefault);
                } else if (mCurrentFrameIndex >= endFrame) { // 情况2: 字已经结束
                    mDestRectF.set(startX, topY, endX, bottomY);
                    mSrcRect.set(0, 0, charEntry.scoreBitmap.getWidth(), charEntry.scoreBitmap.getHeight());
                    canvas.drawBitmap(charEntry.scoreBitmap, mSrcRect, mDestRectF, mBitmapPaint);
                } else { // 情况3: 字正在进行中
                    float cursorX = mCursorPositionX;
                    // a. 绘制左半部分：染色的历史
                    mDestRectF.set(startX, topY, cursorX, bottomY);
                    float timeProgress = (mCurrentFrameIndex - startFrame) / (float) duration;
                    int srcWidth = (int) (charEntry.scoreBitmap.getWidth() * timeProgress);
                    if (srcWidth > 0) {
                        mSrcRect.set(0, 0, srcWidth, charEntry.scoreBitmap.getHeight());
                        canvas.drawBitmap(charEntry.scoreBitmap, mSrcRect, mDestRectF, mBitmapPaint);
                    }
                    // b. 绘制右半部分：白色的未来
                    canvas.drawRect(cursorX, topY, endX, bottomY, mPaintPitchBarDefault);
                }
                canvas.restore(); // 恢复画布，移除裁剪区域

                // 绘制歌词
                mPaintLyric.getTextBounds(charEntry.originalEntry.text, 0, charEntry.originalEntry.text.length(),
                        mTextBounds);
                if (endX - startX > mTextBounds.width()) {
                    canvas.drawText(charEntry.originalEntry.text, (startX + endX) / 2,
                            bottomY + mLyricTextMarginTopPx + mTextBounds.height(), mPaintLyric);
                }
            }
        }
    }

    /**
     * 绘制实时击中时的光圈扩散动画。
     */
    private void drawHitGlowAnimation(Canvas canvas, float userPitchY) {
        long timeSinceHit = System.currentTimeMillis() - mLastHitTime;
        if (timeSinceHit < HIT_GLOW_DURATION) {
            float progress = timeSinceHit / (float) HIT_GLOW_DURATION; // 动画进度 0.0 ~ 1.0
            float radius = mUserPitchBallRadiusPx + dpToPx(20) * progress; // 半径由小变大
            int alpha = (int) (255 * (1 - progress)); // 透明度由255变0
            mPaintHitGlow.setAlpha(alpha);
            canvas.drawCircle(mCursorPositionX, userPitchY, radius, mPaintHitGlow);
        }
    }

    /**
     * 绘制连击数的跳动动画。
     */
    private void drawComboAnimation(Canvas canvas) {
        if (mComboCount > 1) { // Combo数大于1才显示
            String comboText = "x" + mComboCount;
            long timeSinceCombo = System.currentTimeMillis() - mLastComboTime;
            if (timeSinceCombo < COMBO_ANIMATION_DURATION) { // 动画期间
                float progress = timeSinceCombo / (float) COMBO_ANIMATION_DURATION;
                float scale = 1.0f + 0.5f * (float) Math.sin(progress * Math.PI); // 使用正弦曲线实现跳动
                mComboTextSize = spToPx(24) * scale;
            } else { // 动画结束
                mComboTextSize = spToPx(24);
            }
            mPaintComboText.setTextSize(mComboTextSize);
            // 绘制在屏幕右上角
            canvas.drawText(comboText, mViewWidth - dpToPx(20), dpToPx(50), mPaintComboText);
        }
    }

    /**
     * 更新用户音高小球的颜色，如果正在击中音调条，则变为绿色。
     */
    private void updateUserPitchBallColor() {
        boolean isHitting = false;
        if (mCurrentUserPitch > 0) {
            for (Sentence sentence : mSentences) {
                if (mCurrentFrameIndex >= sentence.startFrame && mCurrentFrameIndex < sentence.endFrame) {
                    for (DisplayCharEntry charEntry : sentence.chars) {
                        if (mCurrentFrameIndex >= charEntry.originalEntry.startFrame
                                && mCurrentFrameIndex < charEntry.originalEntry.endFrame) {
                            if (Math.abs(mCurrentUserPitch - charEntry.originalEntry.pitch) <= mPitchHitTolerance) {
                                isHitting = true;
                            }
                            break;
                        }
                    }
                    break;
                }
            }
        }
        mPaintUserPitchBall.setColor(isHitting ? mColorPitchBarHit : mColorCursor);
    }

    // --- 各种工具方法 ---
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
        mCursorPositionX = mViewWidth * mCursorPositionRatio;
    }

    private void drawGuideLines(Canvas canvas) {
        int lineCount = (int) ((mDisplayMaxPitch - mDisplayMinPitch) / 10);
        if (lineCount <= 1)
            return;
        for (int i = 1; i <= lineCount; i++) {
            float pitch = mDisplayMinPitch + i * 10;
            if (pitch >= mDisplayMaxPitch)
                break;
            float y = pitchToY(pitch);
            canvas.drawLine(0, y, mViewWidth, y, mPaintGuideLine);
        }
    }

    private void drawTimeline(Canvas canvas) {
        if (mTimelineTickSeconds <= 0)
            return;
        float currentSeconds = mCurrentFrameIndex / FRAMES_PER_SECOND;
        float leftSeconds = currentSeconds - (mCursorPositionX / mPixelsPerSecond);
        float rightSeconds = currentSeconds + ((mViewWidth - mCursorPositionX) / mPixelsPerSecond);
        long firstTickSecond = (long) (Math.ceil(leftSeconds / mTimelineTickSeconds) * mTimelineTickSeconds);
        for (long tickSecond = firstTickSecond; tickSecond < rightSeconds; tickSecond += mTimelineTickSeconds) {
            if (tickSecond < 0)
                continue;
            long tickFrame = (long) (tickSecond * FRAMES_PER_SECOND);
            float x = frameToX(tickFrame);
            String timeText = String.format("%02d:%02d", tickSecond / 60, tickSecond % 60);
            float y = mViewHeight - dpToPx(5);
            canvas.drawText(timeText, x, y, mPaintTimeline);
        }
    }

    private float frameToX(long frame) {
        return mCursorPositionX + ((frame - mCurrentFrameIndex) / FRAMES_PER_SECOND) * mPixelsPerSecond;
    }

    private long xToFrame(float x) {
        return mCurrentFrameIndex + (long) (((x - mCursorPositionX) / mPixelsPerSecond) * FRAMES_PER_SECOND);
    }

    private float pitchToY(float pitch) {
        float pitchRange = mDisplayMaxPitch - mDisplayMinPitch;
        if (pitchRange <= 0)
            return mViewHeight / 2f;
        pitch = Math.max(mDisplayMinPitch, Math.min(pitch, mDisplayMaxPitch));
        return mViewHeight * (1 - (pitch - mDisplayMinPitch) / pitchRange);
    }

    private float calculateMedian(List<Float> numbers) {
        if (numbers == null || numbers.isEmpty())
            return 0;
        Collections.sort(numbers);
        int middle = numbers.size() / 2;
        if (numbers.size() % 2 == 1)
            return numbers.get(middle);
        else
            return (numbers.get(middle - 1) + numbers.get(middle)) / 2.0f;
    }

    private void calculatePitchDisplayRange() {
        if (mMinPitch == Float.MAX_VALUE || mMaxPitch == Float.MIN_VALUE) {
            mDisplayMinPitch = 40f;
            mDisplayMaxPitch = 80f;
            return;
        }
        mDisplayMinPitch = (float) (Math.floor(mMinPitch / 10.0) * 10);
        mDisplayMaxPitch = (float) (Math.ceil(mMaxPitch / 10.0) * 10);
        mDisplayMinPitch -= 10;
        mDisplayMaxPitch += 10;
        if (mDisplayMaxPitch - mDisplayMinPitch < 30) {
            mDisplayMaxPitch = mDisplayMinPitch + 30;
        }
    }

    private float dpToPx(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }

    private float spToPx(float sp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }

    // --- 内部数据结构 ---
    private enum ScoreState {
        NOT_SCORED, SCORING, HIT, MISS
    }

    /**
     * 包装了原始CharEntry，并增加了打分、动画和离屏缓冲所需的状态。
     */
    private static class DisplayCharEntry {
        final ScoreEngine2.CharEntry originalEntry;
        ScoreState scoreState = ScoreState.NOT_SCORED;
        final List<Float> userPitches = new LinkedList<>();
        Bitmap scoreBitmap; // 离屏画布，用于记录染色历史
        Canvas scoreCanvas;
        private int lastDrawnX = -1; // 用于连接染色点，形成实线
        long scoreFinalizedTime = 0; // 记录分数最终确定的时间戳，用于触发闪烁动画

        DisplayCharEntry(ScoreEngine2.CharEntry entry) {
            this.originalEntry = entry;
            int bitmapWidth = 200; // 离屏Bitmap的宽度，精度足够
            if (entry.startFrame != -1) {
                this.scoreBitmap = Bitmap.createBitmap(bitmapWidth, 1, Bitmap.Config.ARGB_8888);
                this.scoreCanvas = new Canvas(this.scoreBitmap);
                this.scoreState = ScoreState.SCORING;
            }
        }

        void drawScoreSegment(long currentFrame, Paint paint) {
            long duration = originalEntry.endFrame - originalEntry.startFrame;
            if (duration <= 0 || scoreBitmap == null)
                return;
            float progress = (currentFrame - originalEntry.startFrame) / (float) duration;
            int x = (int) (progress * scoreBitmap.getWidth());
            if (x < 0 || x >= scoreBitmap.getWidth())
                return;
            if (lastDrawnX == -1) {
                scoreCanvas.drawPoint(x, 0, paint);
            } else if (x > lastDrawnX) {
                scoreCanvas.drawLine(lastDrawnX, 0, x, 0, paint);
            }
            lastDrawnX = x;
        }

        void finalizeScore(Paint paint) {
            if (scoreCanvas != null) {
                Paint fillPaint = new Paint();
                fillPaint.setColor(paint.getColor());
                fillPaint.setStyle(Paint.Style.FILL);
                scoreCanvas.drawRect(0, 0, scoreBitmap.getWidth(), 1, fillPaint);
            }
            this.scoreState = (paint.getColor() == Color.GREEN) ? ScoreState.HIT : ScoreState.MISS;
        }

        void setFinalStateOnly(boolean isHit) {
            this.scoreState = isHit ? ScoreState.HIT : ScoreState.MISS;
        }

        boolean isScored() {
            return scoreState == ScoreState.HIT || scoreState == ScoreState.MISS;
        }

        void recycle() {
            if (scoreBitmap != null && !scoreBitmap.isRecycled()) {
                scoreBitmap.recycle();
                scoreBitmap = null;
            }
            scoreCanvas = null;
        }
    }

    /**
     * 代表一句歌词，包含多个字。
     */
    private static class Sentence {
        final List<DisplayCharEntry> chars;
        final long startFrame, endFrame;

        Sentence(List<DisplayCharEntry> charEntries) {
            this.chars = Collections.unmodifiableList(charEntries);
            if (charEntries.isEmpty()) {
                this.startFrame = -1;
                this.endFrame = -1;
            } else {
                this.startFrame = charEntries.get(0).originalEntry.startFrame;
                this.endFrame = charEntries.get(charEntries.size() - 1).originalEntry.endFrame;
            }
        }
    }
}